package bb.lanxing.nav;

import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;

import com.alibaba.fastjson.JSONObject;
import com.baidu.mapapi.model.LatLng;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Stack;

import bb.lanxing.common.config.Constants;
import bb.lanxing.model.database.RouteBook;
import bb.lanxing.model.database.RouteBookPoint;
import bb.lanxing.model.json.Route;
import bb.lanxing.model.json.RoutePoint;
import bb.lanxing.model.json.RoutePolyline;
import bb.lanxing.model.json.RouteStep;
import bb.lanxing.util.BiCiCoorConverter;
import bb.lanxing.util.AppFileUtils;
import bb.lanxing.util.Log;
import bb.lanxing.util.PolyUtil;
import bb.lanxing.util.map.SphericalUtil;

/**
 * 导航引擎
 */
public class NavEngine {
    private static final String TAG = "NavEngine";

    private static final Integer[] FORECAST_THRESHOLD = {500, 200, 50};

    private static NavEngine instance;

    private NavStatusListener mNavStatusListener;
    // 导航计算线程
    private HandlerThread handlerThread;
    private Handler calHandler;
    // 主线程 Handler
    private Handler handler;
    // 是否还在导航
    private boolean isNavigating;
    // 是否已初始化成功
    private boolean initialized = false;
    // 规划的路径
    private Route route;
    // 路径终点经纬度
    private LatLng endPoint;
    // 上次导航时间
    private long lastLocatedTime;
    // 已走过的节点
    private Stack<RouteStep> passedSteps;
    // 还没走过的节点
    private Stack<RouteStep> frontSteps;
    // 当前位置在路径规划的哪个节点
    private RouteStep curStep;
    // 当前节点的距离
    private int lastVoiceDistance;
    // 不知道干啥的
    private boolean hasForecast;
    // 不知道干啥的
    private boolean onNewStep;
    // 是否第一个节点
    private boolean firstStep;
    // 不知道干啥的
    private LinkedList<Integer> forecastList;

    private LinkedList<LatLng> lastPath;

    private boolean locYaw;

    private int yawCount;

    private NavEngine() {
        if (handlerThread == null || !handlerThread.isAlive()) {
            handlerThread = new HandlerThread("nav-calculate");
            handlerThread.start();
            calHandler = new Handler(handlerThread.getLooper());
            handler = new Handler(Looper.getMainLooper());
        }
    }

    public static NavEngine getInstance() {
        if (instance == null) {
            instance = new NavEngine();
        }
        return instance;
    }

    public void setNavigationStatueListener(NavStatusListener navStatusListener) {
        mNavStatusListener = navStatusListener;
    }

    public void setupRoute(final long lushuId) {
        if (lushuId > 0 && calHandler != null) {
            calHandler.post(() -> {
                List<RouteBookPoint> lushuPointList = RouteBookPoint.getByLushuId(lushuId);
                if (lushuPointList == null || lushuPointList.isEmpty()) {
                    return;
                }
                ArrayList<LatLng> lushuLatLngList = new ArrayList<>(lushuPointList.size());
                for (RouteBookPoint lushuPoint : lushuPointList) {
                    lushuLatLngList.add(lushuPoint.getLatLng());
                }
                Route analyse = new PolyAnalyzer().analyse(lushuLatLngList);
                saveRouteToFile(analyse, RouteBook.getById(lushuId));
                setupRoute(analyse);
            });
        }
    }

    private void saveRouteToFile(Route route, RouteBook routeBook) {
        JSONObject jSONObject = new JSONObject();
        jSONObject.put("routes", new Route[]{route});
        String filePath = AppFileUtils.buildExternalDirectoryPath(Constants.LUSHU_DIRECTION) + (routeBook.getUuid() + ".bb");
        Log.d(TAG, "saveDirectionFile, filePath = " + filePath + ", md5 = " + AppFileUtils.buildGZIPFile(jSONObject.toJSONString(), filePath));
    }

    private void setupRoute(Route route) {
        if (route == null) {
            return;
        }
        this.route = route;
        RoutePoint[] overviewPath = route.getOverviewPath();
        RoutePolyline overviewPolyline = route.getOverviewPolyline();
        if (overviewPath != null) {
            RoutePoint routePoint = overviewPath[overviewPath.length - 1];
            endPoint = new LatLng(routePoint.getLat(), routePoint.getLng());
        } else if (overviewPolyline != null) {
            List<LatLng> polyPath = overviewPolyline.getPolyPath();
            endPoint = polyPath.get(polyPath.size() - 1);
        } else {
            if (mNavStatusListener != null) {
                mNavStatusListener.onNavFailed(40);
            }
            List<RouteStep> allSteps = route.getAllSteps();
            if (allSteps.isEmpty()) {
                if (mNavStatusListener != null) {
                    mNavStatusListener.onNavFailed(51);
                }
                Log.w(TAG, "setupRoute: total steps is empty ! exit !");
                return;
            }
            endPoint = allSteps.get(allSteps.size() - 1).getEndLoc().toLatLng();
        }
        lastPath = new LinkedList<>();
        if (mNavStatusListener != null) {
            mNavStatusListener.onSetup(route);
        }
        initialized = false;
        isNavigating = true;
        Log.d(TAG, "setupRoute - " + route);
    }

    public boolean isNavigating() {
        return isNavigating;
    }

    public void onRefreshLocation(final double lat, final double lng, final float speed) {
        if (!isNavigating) {
            return;
        }
        long currentTimeMillis = System.currentTimeMillis();
        if (currentTimeMillis - lastLocatedTime < 1000) {
            return;
        }
        lastLocatedTime = currentTimeMillis;
        if (calHandler != null) {
            if (!initialized) {
                calHandler.post(() -> {
                    initSteps(route, new LatLng(lat, lng));
                    if (handler != null) {
                        handler.post(() -> {
                            if (mNavStatusListener == null || frontSteps.isEmpty()) {
                                return;
                            }
                            mNavStatusListener.onInitialized(curStep, frontSteps.peek());
                        });
                    }
                });
            } else {
                calHandler.post(() -> calCurrentPosition(new LatLng(lat, lng), speed));
            }
        }
    }

    private void initSteps(Route route, LatLng location) {
        List<RouteStep> allSteps = route.getAllSteps();
        if (allSteps.isEmpty()) {
            if (mNavStatusListener != null) {
                mNavStatusListener.onNavFailed(51);
            }
            Log.w(TAG, "initSteps: all steps is empty ! exit !");
            return;
        }
        int minDistance = Integer.MAX_VALUE;
        int minDistanceStep = 0;
        for (int i = 0; i < allSteps.size(); i++) {
            int calMinDistance = allSteps.get(i).calMinDistance(location);
            if (calMinDistance < minDistance) {
                minDistanceStep = i;
                minDistance = calMinDistance;
            }
        }
        if (minDistance > 30) {
            Log.w(TAG, "current location is far away from the route, min distance = " + minDistance);
        }
        curStep = allSteps.get(minDistanceStep);
        lastVoiceDistance = Math.round(curStep.getDistance().getValue() / 1000);
        hasForecast = false;
        onNewStep = true;
        firstStep = true;
        forecastList = new LinkedList<>(Arrays.asList(FORECAST_THRESHOLD));
        passedSteps = new Stack<>();
        for (int i = 0; i < minDistanceStep; i++) {
            passedSteps.push(allSteps.get(i));
        }
        frontSteps = new Stack<>();
        for (int size = allSteps.size() - 1; size > minDistanceStep; size--) {
            frontSteps.push(allSteps.get(size));
        }
        initialized = true;
        Log.d(TAG, "nav engine first init finished. current step index = " + minDistanceStep + " min distance = " + minDistance);
    }

    private void runOnMainThread(Runnable runnable) {
        if (handler != null) {
            handler.post(runnable);
        }
    }

    private void calCurrentPosition(final LatLng loc, final float speed) {
        if (SphericalUtil.computeDistanceBetween(endPoint, loc) < 50.0d) {
            closeToDestination();
            Log.d(TAG, "close to end point.");
            return;
        }
        lastPath.addFirst(loc);
        if (lastPath.size() > 100) {
            lastPath.removeLast();
        }
        if (onPolyline(loc, curStep.getPolyPath())) {
            if (locYaw) {
                locYaw = false;
                runOnMainThread(() -> mNavStatusListener.onYawBack(curStep));
            }
            if (onPolyline(endPoint, lastPath)) {
                closeToDestination();
                Log.d(TAG, "end points on the latest path.");
            } else if (!frontSteps.isEmpty()) {
                final RouteStep peek = frontSteps.peek();
                boolean onPolyline = onPolyline(loc, peek.getPolyPath(), 50.0d);
                LatLng common2Earth = BiCiCoorConverter.common2Earth(curStep.getEndLoc().toLatLng());
                final double computeDistanceBetween = SphericalUtil.computeDistanceBetween(loc, common2Earth);
                if (onNewStep && !firstStep) {
                    double computeDistanceBetween2 = SphericalUtil.computeDistanceBetween(BiCiCoorConverter.common2Earth(curStep.getStartLoc().toLatLng()), common2Earth);
                    Log.d("nav", "new step distance = " + computeDistanceBetween + " step distance = " + computeDistanceBetween2);
                    if (10.0d + computeDistanceBetween < computeDistanceBetween2) {
                        runOnMainThread(() -> {
                            if (mNavStatusListener != null) {
                                Log.d(NavEngine.TAG, "current step changed. current step = " + curStep);
                                mNavStatusListener.onStepChanged(curStep, peek, false);
                            }
                        });
                        onNewStep = false;
                    }
                }
                if (hasForecast) {
                    if (!forecastList.isEmpty() && computeDistanceBetween < calcForecastDistance(forecastList.peek(), speed)) {
                        forecastList.remove();
                        runOnMainThread(() -> {
                            if (mNavStatusListener != null) {
                                mNavStatusListener.onStepChanged(curStep, peek, true);
                            }
                        });
                    }
                    runOnMainThread(() -> {
                        RouteStep routeStep;
                        if (mNavStatusListener != null) {
                            if (route.getPlanSource() == Route.PlanSource.Baidu) {
                                routeStep = curStep;
                            } else {
                                routeStep = peek;
                            }
                            Log.d(NavEngine.TAG, "step info update. current step = " + curStep + ", distance = " + computeDistanceBetween);
                            mNavStatusListener.onUpdateInfo(routeStep, computeDistanceBetween);
                        }
                    });
                } else {
                    int formatDistance2KM = formatDistance2KM(computeDistanceBetween);
                    int i = lastVoiceDistance - formatDistance2KM;
                    if (i > 1 && i <= 2) {
                        lastVoiceDistance = formatDistance2KM;
                        runOnMainThread(() -> {
                            if (mNavStatusListener != null) {
                                Log.d(NavEngine.TAG, "along current step go ahead. current step = " + curStep);
                                mNavStatusListener.onKeepGoing(curStep, computeDistanceBetween);
                            }
                        });
                    }
                    int intValue = forecastList.peek();
                    float calcForecastDistance = calcForecastDistance(intValue, speed);
                    RouteStep.IntValuePair distance = curStep.getDistance();
                    if (distance.getValue() / 2 < calcForecastDistance) {
                        calcForecastDistance = distance.getValue() / 2;
                    }
                    if (!hasForecast && (onPolyline || computeDistanceBetween < calcForecastDistance)) {
                        hasForecast = true;
                        final RouteStep routeStep = curStep;
                        runOnMainThread(() -> {
                            if (mNavStatusListener != null) {
                                Log.d(NavEngine.TAG, "current step changed. last step = " + routeStep + " current step = " + curStep);
                                mNavStatusListener.onStepChanged(routeStep, peek, true);
                            }
                        });
                        forecastList.remove();
                        while (!forecastList.isEmpty() && (forecastList.peek() >= computeDistanceBetween || Math.abs(forecastList.peek() - intValue) < 40)) {
                            forecastList.remove();
                        }
                    }
                }
                if (onPolyline) {
                    passedSteps.push(curStep);
                    curStep = frontSteps.pop();
                    lastVoiceDistance = formatDistance2KM(curStep.getDistance().getValue());
                    forecastList = new LinkedList<>(Arrays.asList(FORECAST_THRESHOLD));
                    onNewStep = true;
                    firstStep = false;
                    hasForecast = false;
                    runOnMainThread(() -> {
                        RouteStep routeStep2;
                        if (mNavStatusListener != null) {
                            if (route.getPlanSource() == Route.PlanSource.Baidu) {
                                routeStep2 = curStep;
                            } else {
                                routeStep2 = peek;
                            }
                            mNavStatusListener.onUpdateInfo(routeStep2, -1.0d);
                        }
                    });
                }
            }
            yawCount = 0;
            return;
        }
        final boolean searchCurrentSteps = searchCurrentSteps(loc);
        runOnMainThread(() -> {
            if (mNavStatusListener != null) {
                if (searchCurrentSteps) {
                    Log.d(NavEngine.TAG, "search current step, step = " + curStep + ",  locYaw = " + locYaw);
                    yawCount = 0;
                    if (locYaw) {
                        locYaw = false;
                        mNavStatusListener.onYawBack(curStep);
                        return;
                    }
                    mNavStatusListener.onStepChanged((passedSteps == null || passedSteps.isEmpty()) ? null : passedSteps.peek(), curStep, false);
                } else if (access(NavEngine.this) < 5) {
                } else {
                    Log.d(NavEngine.TAG, "current location yaw !!!" + curStep);
                    yawCount = 0;
                    locYaw = true;
                    mNavStatusListener.onYaw(curStep);
                }
            }
        });
    }

    private void closeToDestination() {
        if (handler != null) {
            handler.post(() -> {
                if (mNavStatusListener != null) {
                    Log.d(NavEngine.TAG, "arrived destination !");
                    mNavStatusListener.onArrived();
                    isNavigating = false;
                }
            });
        }
    }

    private static boolean onPolyline(LatLng point, List<LatLng> polyline) {
        return PolyUtil.isLocationOnEdge(point, polyline, true, 30.0d);
    }

    private static boolean onPolyline(LatLng point, List<LatLng> polyline, double threshold) {
        return PolyUtil.isLocationOnEdge(point, polyline, true, threshold);
    }

    private float calcForecastDistance(int distance, float speed) {
        if (speed < 0.0f) {
            speed = 0.0f;
        }
        if (speed > 12.0f) {
            speed = 12.0f;
        }
        return distance + (speed * 2.0f);
    }

    private int formatDistance2KM(double distance) {
        return (int) Math.round(distance / 1000);
    }

    private boolean searchCurrentSteps(LatLng loc) {
        long currentTimeMillis = System.currentTimeMillis();
        List<RouteStep> allSteps = route.getAllSteps();
        int size = allSteps.size();
        if (size == 0) {
            Log.w(TAG, "searchCurrentSteps: all steps is empty ! exit !");
            return false;
        }
        int size2 = passedSteps.size() + 1;
        int size3 = passedSteps.size() - 1;
        while (true) {
            if (size2 >= size && size3 < 0) {
                size3 = -1;
                break;
            } else if (size2 < size && onPolyline(loc, allSteps.get(size2).getPolyPath())) {
                size3 = size2;
                break;
            } else if (size3 >= 0 && size3 < allSteps.size() && onPolyline(loc, allSteps.get(size3).getPolyPath())) {
                break;
            } else {
                size2++;
                size3--;
            }
        }
        int i = 0;
        if (size3 >= 0) {
            curStep = allSteps.get(size3);
            lastVoiceDistance = formatDistance2KM(curStep.getDistance().getValue());
            hasForecast = false;
            forecastList = new LinkedList<>(Arrays.asList(FORECAST_THRESHOLD));
            while (size2 < size3) {
                passedSteps.push(allSteps.get(i));
                i++;
            }
            for (int size4 = allSteps.size() - 1; size4 > size3; size4--) {
                frontSteps.push(allSteps.get(size4));
            }
            Log.d(TAG, "search current steps cost = " + (System.currentTimeMillis() - currentTimeMillis));
            return true;
        }
        if (frontSteps.isEmpty()) {
            lastPath.clear();
            lastPath.add(loc);
        }
        Log.d(TAG, "search current steps cost = " + (System.currentTimeMillis() - currentTimeMillis));
        return false;
    }

    private int access(NavEngine navEngine) {
        int i = navEngine.yawCount + 1;
        navEngine.yawCount = i;
        return i;
    }

    public void stopNavigation() {
        if (isNavigating) {
            if (frontSteps != null) {
                frontSteps.clear();
            }
            if (passedSteps != null) {
                passedSteps.clear();
            }
            route = null;
            isNavigating = false;
            Log.d(TAG, "navigation end !");
        }
    }

    public static void release() {
        if (instance != null) {
            instance.stopNavigation();
            if (instance.handlerThread != null) {
                instance.handlerThread.quit();
            }
            instance.handlerThread = null;
            instance = null;
        }
    }
}